package applicationFunction.socks;

import java.net.*;
import java.io.*;
import java.util.Arrays;
import java.util.HashMap;

public class SocksClientDemo extends Thread {
    // Some useful SOCKS constant

    static final int PROTO_VERS4 = 4;
    static final int PROTO_VERS = 5;
    static final int DEFAULT_PORT = 1080;


    enum Methods {
        NO_AUTH(0), GSSAPI(1), USER_PASSW(2), NO_METHODS(-1);
        int value;

        Methods(int i) {
            value = i;
        }
        public static Methods getMethods(int value){
            for(Methods method : Methods.values()){
                if(method.value == value){
                    return method;
                }
            }
            return NO_AUTH;
        }
    }

    enum AuthenticationStatus {
        SUCCESS(0);
        int status;

        AuthenticationStatus(int status) {
            this.status = status;
        }
    }


    enum CMD {
        CONNECT(1), BIND(2), UDP_ASSOCIATE(3);
        int flag;

        CMD(int flag) {
            this.flag = flag;
        }
    }

    /**
     * address type of following address
     */
    enum ATYP {
        IPV4(1), DOMAIN_NAME(3), IPV6(4);

        int flag;

        ATYP(int flag) {
            this.flag = flag;
        }
        public static ATYP getATPY(int flag){
            for(ATYP atyp : ATYP.values()){
                if(atyp.flag == flag){
                    return atyp;
                }
            }
            return IPV4;
        }
    }


    enum REPLIES {
        SUCCEEDED(0),
        FAILURE(1),//general SOCKS server failure
        NOT_ALLOWED(2),//connection not allowed by ruleset
        NET_UNREACHABLE(3),
        HOST_UNREACHABLE(4),
        CONN_REFUSED(5),
        TTL_EXPIRED(6),
        CMD_NOT_SUPPORTED(7),
        ADDR_TYPE_NOT_SUP(8);

        int status;

        REPLIES(int status) {
            this.status = status;
        }
    }


    private InetAddress destAdress;//目标地址
    private int destPort;//目标端口
    private Socket proxy;//代理的socks
    private InputStream in;
    private OutputStream out;
    private String uname;
    private String passwd;
    private CMD cmd;
    private ATYP atyp;
    private Methods[] methods;//所有的加密方法
    private Methods method;//服务端选择的加密方法
    private String bndAddr;
    private int bndPort;

    private Socket orgin;//原始未封装的socket
    private boolean useV4 = false;
    private HashMap<String, String> users = new HashMap<>();
    private volatile boolean done = false;


    public SocksClientDemo(Socket orgin, InetAddress destAdress, int port,
                           boolean v4) throws IOException {
        this(orgin, destAdress, port);
        this.useV4 = v4;
    }

    public SocksClientDemo(Socket orgin, InetAddress destAdress, int port) throws IOException {
        proxy = new Socket(destAdress, port);
        this.orgin = orgin;
        this.destAdress = destAdress;
        this.destPort = port;
        this.in = proxy.getInputStream();
        this.out = proxy.getOutputStream();
    }


    public void addUser(String user, String passwd) {
        users.put(user, passwd);
    }


    public void close() {
        try {
            proxy.close();
        } catch (IOException unused) {
        }
    }


    /**
     * 建立连接，然后获取服务端给的加密算法是哪个。
     * @return f返回选择的校验方法
     * @throws IOException
     */
    private int establishConnect() throws IOException {
        out.write(getVersion());
        int n = methods.length;
        out.write(n);
        for (int i = 0; i < n; i++) {
            out.write(methods[i].value);
        }
        out.flush();
        int ver = in.read();
        int method = in.read();
        this.method = Methods.getMethods(method);
        return method;
    }

    /**
     * 验证
     */
    private boolean userPassAuth() throws IOException {
        out.write(getVersion());
        byte[] uname = this.uname.getBytes();
        int ulen = uname.length;
        byte[] passwd = this.passwd.getBytes();
        int plen = passwd.length;

        out.write(ulen);
        out.write(uname);
        out.write(plen);
        out.write(passwd);
        out.flush();

        int ver = in.read();
        int status = in.read();
        if (ver == getVersion() && (status == AuthenticationStatus.SUCCESS.status)) {
            return true;
        }

        return false;
    }

    /**
     * Send the SOCKS v5 requests
     */
    private int getRequest() throws IOException {
        out.write(getVersion());
        out.write(cmd.flag);
        int rsv = 0;//貌似没啥用
        out.write(rsv);

        out.write(atyp.flag);
        switch (atyp) {
            case IPV4: {
                out.write(atyp.flag);
                byte[] buf = new byte[4];
                out.write(destAdress.getAddress());

            }
            break;
            case DOMAIN_NAME: {
                int length = destAdress.getAddress().length;
                out.write(length);
                out.write(destAdress.getAddress());
            }
            break;
            case IPV6: {
                out.write(destAdress.getAddress());
            }
            break;
        }

        out.write(destPort);
        out.flush();

        //服务器返回的消息
        int ver = in.read();
        int rep = in.read();
        if (ver == -1 || rep == -1) {
            in.close();
            out.close();
            return rep;
        }
        int rRsv = in.read();
        int atyp = in.read();
        ATYP temp = ATYP.getATPY(atyp);
        switch(temp) {
            case IPV4:
            {
                byte[] buf = new byte[4];
                readBuf(in, buf);
                int i = 0;
                StringBuffer sb = new StringBuffer();
                for (i = 0; i < 4; i++) {
                    sb.append(buf[i]&0xff);
                    if (i < 3)
                        sb.append('.');
                }
                bndAddr = sb.toString();
            }
            break;
            case DOMAIN_NAME:
            {
                int i = in.read();
                byte[] buf = new byte[i];
                readBuf(in, buf);
                bndAddr = new String(buf);
            }
            break;
            case IPV6:
            {
                byte[] buf = new byte[16];
                readBuf(in, buf);
                int i = 0;
                StringBuffer sb = new StringBuffer();
                for (i = 0; i<16; i++) {
                    sb.append(Integer.toHexString(buf[i]&0xff));
                    if (i < 15)
                        sb.append(':');
                }
                bndAddr = sb.toString();
            }
            break;
        }
        bndPort = in.read();
        return rep;
    }




    /**
     * 打开管道
     */
    private void openChannel() throws IOException {
        InputStream oIn = orgin.getInputStream();
        OutputStream oOut = orgin.getOutputStream();
        Tunnel tunnel = new Tunnel(oIn, out);
        tunnel.start();

        int b = 0;
        do {
            // Note that the socket might be closed from another thread (the tunnel)
            try {
                b = in.read();
                if (b == -1) {
                    in.close();
                    oOut.close();
                    return;
                }
                oOut.write(b);
            } catch(IOException ioe) {
            }
        } while (!orgin.isClosed());
    }


    public void sendSocks() throws IOException {
        setMethods(new Methods[]{Methods.NO_AUTH});
        setAtyp(ATYP.IPV4);
        setCmd(CMD.CONNECT);
        establishConnect();
        if(method.equals(Methods.USER_PASSW)){
            setUnamePasswd("ylw","123456");
            userPassAuth();
        }
        getRequest();
        openChannel();

    }

    private int getVersion() {
        if (useV4) {
            return PROTO_VERS4;
        } else {
            return PROTO_VERS;
        }
    }

    private void readBuf(InputStream is, byte[] buf) throws IOException {
        int l = buf.length;
        int count = 0;
        int i;
        do {
            i = is.read(buf, count, l - count);
            if (i == -1)
                throw new IOException("unexpected EOF");
            count += i;
        } while (count < l);
    }


    /**
     * 管道 用来传输字节 类似与 linux中的 | 符号
     */
    class Tunnel extends Thread {
        private InputStream tin;
        private OutputStream tout;

        Tunnel(InputStream in, OutputStream out) {
            tin = in;
            tout = out;
        }

        public void run() {
            int b;
            while (true) {
                try {
                    b = tin.read();
                    if (b == -1) {
                        tin.close();
                        tout.close();
                        return;
                    }
                    tout.write(b);
                } catch (IOException e) {
                    // actually exit from the thread
                    return;
                }
            }
        }
    }


    public void setMethods(Methods[] methods) {
        Arrays.sort(methods);
        this.methods = methods;
    }

    public void setUnamePasswd(String uname,String passwd){
        this.uname = uname;
        this.passwd = passwd;
    }

    public  void setCmd(CMD cmd){
        this.cmd = cmd;
    }

    public void setAtyp(ATYP atyp){
        this.atyp = atyp;
    }

    public static void main(String[] args) throws IOException,
            InterruptedException {
        InetSocketAddress inetAddress = new InetSocketAddress("127.0.0.1",6666);

        Socket socket = new Socket("127.0.0.1", 8888);
        SocksClientDemo clientDemo = new SocksClientDemo(socket,
                inetAddress.getAddress(), inetAddress.getPort());
        clientDemo.sendSocks();
//        OutputStream out = socket.getOutputStream();
//        while (true) {
//
//            out.write('A');
//            Thread.sleep(1000);
//        }
    }
}
